package com.jappyframework;

import java.io.StringWriter;
import java.util.Set;
import java.util.TreeSet;

/**
 * A class to help with generating nicely-formatted Java source files. This
 * class does not handle all possible situations; the caller should expect to
 * mingle calls to this class with calls directly to the underlying PrintWriter.
 * <p>
 * This class does not have any validation intelligence, and does not attempt to
 * protect the caller from generating bogus code.
 * 
 * NOTE: Modifications done so that this writes directly to a String.
 */
public class JavaSource {

	public enum AccessSpecifier {
		DEFAULT, PRIVATE, PROTECTED, PUBLIC;

		public String getKeyword() {
			switch (this) {
			default:
				return "";
			case PRIVATE:
				return "private";
			case PROTECTED:
				return "protected";
			case PUBLIC:
				return "public";
			}
		}
	}

	/**
	 * Often imports need to be added later. Hence we use two writers
	 * one for package + imports and one for the rest of the code.
	 * 
	 * TODO: Generalize this.
	 */
	protected final StringWriter imports_out_;
	protected final StringWriter code_out_;
	protected final String indent_;
	protected int indentLevel_ = 0;

	private Set<String> imports;
	
	/**
	 * Construct a Java source helper with a customized indentation string
	 * 
	 * @param out
	 *            the PrintWriter the output will be sent to
	 * @param indentString
	 *            the string defining a single indent, e.g., <code>"\t"</code>
	 *            or <code>"   "</code>. Can be empty or null.
	 */
	public JavaSource(String indentString) {
		indent_ = indentString == null ? "" : indentString;
		imports_out_ = new StringWriter();
		code_out_ = new StringWriter();
		imports = new TreeSet<String>();
	}

	/**
	 * Construct a Java source helper that uses a default tab indentation
	 * 
	 * @param out
	 *            the PrintWriter the output will be sent to
	 */
	public JavaSource() {
		this("\t");
	}

	private void newLineInternal(StringWriter writer) {
		writer.write("\n");
	}
	
	public void newLine() {
		newLineInternal(code_out_);
	}

	/**
	 * Print the package statement.
	 * 
	 * @param pkg
	 *            the package name, e.g., "org.example.test"
	 */
	public void printPackage(String pkg) {
		assert pkg != null && pkg.length() > 0;
		imports_out_.write("package " + pkg + ";");
		newLineInternal(imports_out_);
	}

	/**
	 * Print an import statement.
	 * 
	 * @param imprt
	 *            the class or group to import, e.g., "org.xyz.Foo" or
	 *            "org.xyz.*"
	 */
	public void printImport(String imprt) {
		assert imprt != null && imprt.length() > 0;
		if (imports.contains(imprt)) {
			return;
		}
		imports_out_.write("import " + imprt + ";");
		imports.add(imprt);
		newLineInternal(imports_out_);
	}

	/**
	 * Generate the first line of a class declaration. This works for top-level
	 * or nested classes. This method increases the indent level by one.
	 * 
	 * @param isAbstract
	 *            if true, class will be marked as abstract
	 * @param accessSpecifier
	 *            the access specifier of this class, e.g.,
	 *            AccessSpecifier.PUBLIC
	 * @param name
	 *            the simple name of this class
	 * @param extendName
	 *            the simple name of a class that this class extends, or null if
	 *            there is none
	 * @param interfaces
	 *            an array of simple names of interfaces that this class
	 *            implements, or null if there are none
	 */
	public void openClass(boolean isAbstract, AccessSpecifier as, String name,
			String extendName, String[] interfaces) {
		assert name != null && name.length() > 0;
		printIndent();
		StringBuilder sb = new StringBuilder();
		sb.append(as.getKeyword());
		if (sb.length() > 0)
			sb.append(' ');
		if (isAbstract) {
			sb.append("abstract ");
		}
		sb.append("class ");
		sb.append(name);
		if (extendName != null && extendName.length() > 0) {
			sb.append(" extends ");
			sb.append(extendName);
		}
		if (interfaces != null) {
			for (int i = 0; i < interfaces.length; ++i) {
				String intfc = interfaces[i];
				if (intfc.length() == 0)
					continue;
				if (i == 0) {
					sb.append(" implements ");
				} else {
					sb.append(", ");
				}
				sb.append(intfc);
			}
		}
		sb.append(" {");
		code_out_.write(sb.toString());
		newLineInternal(code_out_);
		increaseIndent();
	}

	/**
	 * Generate the last line of a class, interface, or method declaration.
	 * There must be one call to this method for every call to openClass() or
	 * openInterface(). There must also be one call to this method for every
	 * call to openMethod() for methods that have bodies. This method decreases
	 * the indent level by one.
	 */
	public void closeElement() {
		decreaseIndent();
		printIndent();
		code_out_.write("}");
		newLineInternal(code_out_);
	}

	/**
	 * Generate the first line of an interface declaration. This works for
	 * top-level or nested classes. This method increases the indent level by
	 * one.
	 * 
	 * @param accessSpecifier
	 *            the access specifier of this interface, e.g.,
	 *            AccessSpecifier.PUBLIC
	 * @param name
	 *            the simple name of this interface
	 * @param extendNames
	 *            an array of simple names of interfaces that this interface
	 *            extends, or null if there are none
	 */
	public void openInterface(AccessSpecifier as, String name,
			String[] extendNames) {
		assert name != null && name.length() > 0;
		StringBuilder sb = new StringBuilder();
		sb.append(as.getKeyword());
		if (sb.length() > 0)
			sb.append(' ');
		sb.append("interface ");
		sb.append(name);
		if (extendNames != null && extendNames.length > 0) {
			sb.append(" extends ");
			for (int i = 0; i < extendNames.length; ++i) {
				if (i > 0) {
					sb.append(", ");
				}
				sb.append(extendNames[i]);
			}
			sb.append(' ');
		}
		sb.append('{');
		printIndent();
		code_out_.write(sb.toString());
		newLineInternal(code_out_);
		increaseIndent();
	}

	/**
	 * Generate the first line of a method declaration. This method increases
	 * the indent level by one, if <code>hasBody</code> is true. Every call to
	 * this method must be matched by a call to closeElement(), unless
	 * <code>hasBody</code> is false.
	 * 
	 * @param isAbstract
	 *            true if this method is abstract. Should be <code>false</code>
	 *            for interfaces and constructors.
	 * @param accessSpecifier
	 *            e.g., AccessSpecifier.PUBLIC
	 * @param returnType
	 *            the return type of the method. Use null for constructors.
	 * @param name
	 *            the name of the method. For constructors, use the type name.
	 * @param paramTypes
	 *            the simple names of the parameter types. Must be the same
	 *            length as <code>argNames</code>.
	 * @param params
	 *            the names of the parameters. Must be same length as
	 *            <code>types</code>.
	 * @param hasBody
	 *            true if this method has a body, i.e., is in a class rather
	 *            than an interface and is not abstract. Ignored if the method
	 *            is abstract.
	 */
	public void openMethod(boolean isAbstract, AccessSpecifier as,
			String returnType, String name, String[] paramTypes,
			String[] params, String[] exceptionTypes, boolean hasBody) {
		assert name != null && name.length() > 0;
		assert (paramTypes == null && params == null)
				|| (paramTypes.length == params.length);
		if (isAbstract) {
			hasBody = false;
		}
		StringBuilder sb = new StringBuilder();
		sb.append(as.getKeyword());
		if (sb.length() > 0)
			sb.append(' ');
		if (isAbstract) {
			sb.append("abstract ");
		}
		if (returnType != null && returnType.length() != 0) {
			sb.append(returnType);
		}
		sb.append(' ');
		sb.append(name);
		sb.append('(');

		if (paramTypes != null) {
			for (int i = 0; i < paramTypes.length; ++i) {
				if (i > 0) {
					sb.append(", ");
				}
				sb.append(paramTypes[i]);
				sb.append(' ');
				sb.append(params[i]);
			}
		}

		sb.append(')');
		if (exceptionTypes != null && exceptionTypes.length != 0) {
			sb.append(" throws ");
			for (int i = 0; i < exceptionTypes.length; ++i) {
				if (i > 0) {
					sb.append(", ");
				}
				sb.append(exceptionTypes[i]);
			}
		}
		if (hasBody) {
			sb.append(" {");
		} else {
			sb.append(';');
		}
		printIndent();
		code_out_.write(sb.toString());
		newLineInternal(code_out_);
		if (hasBody) {
			increaseIndent();
		}
	}

	/**
	 * Generate a field declaration
	 * 
	 * @param as
	 *            the access specifier (e.g., AccessSpecifier.PRIVATE)
	 * @param isStatic
	 *            true if this is a static field
	 * @param isFinal
	 *            true if this field is final
	 * @param the
	 *            type of the field, e.g. "String"
	 * @param name
	 *            the simple name of the field
	 * @param initializer
	 *            the initializer, not including '=', e.g. <code>"foo"</code>.
	 *            Note that if this is a String initializer the quotes must be
	 *            explicitly included. for a String field; or null if none is
	 *            needed.
	 */
	public void printField(AccessSpecifier as, boolean isStatic,
			boolean isFinal, String type, String name, String initializer) {
		assert name != null && name.length() > 0;
		StringBuilder sb = new StringBuilder();
		sb.append(as.getKeyword());
		if (sb.length() > 0)
			sb.append(' ');
		if (isStatic) {
			sb.append("static ");
		}
		if (isFinal) {
			sb.append("final ");
		}
		sb.append(type);
		sb.append(' ');
		sb.append(name);
		if (initializer != null && initializer.length() > 0) {
			sb.append(" = ");
			sb.append(initializer);
		}
		sb.append(';');
		printIndent();
		code_out_.write(sb.toString());
		newLineInternal(code_out_);
	}

	/**
	 * Print an arbitrary line of text, prefixed by the proper indentation. A
	 * newline character will be appended to the printed text.
	 */
	public void printText(String text) {
		assert text != null && text.length() > 0;
		printIndent();
		code_out_.write(text);
		newLineInternal(code_out_);
	}

	/**
	 * Increase the indentation level. This is done automatically when methods
	 * and types are opened, but may be done explicitly, e.g. when opening a
	 * block of code within a method body. Every call to this method must be
	 * matched with a call to decreaseIndent().
	 */
	public void increaseIndent() {
		++indentLevel_;
	}

	/**
	 * Decrease the indentation level. This is done automatically when closing a
	 * method or type, but should be done explicitly whenever increaseIndent()
	 * has been called, e.g. to close a block of code in a method body. Every
	 * call to this method must be matched with a call to increaseIndent().
	 */
	public void decreaseIndent() {
		--indentLevel_;
		assert indentLevel_ >= 0;
	}

	/**
	 * Print the proper amount of indentation.
	 */
	protected void printIndent() {
		for (int i = 0; i < indentLevel_; ++i) {
			code_out_.write(indent_);
		}
	}

	public String getSourceCode() {
		return imports_out_.toString() + "\n\n" + code_out_.toString();
	}

}
